<template>
  <div class="container">
    <el-container>
      <el-header class="header-buttons">
        <el-button type="primary" @click="newFile">新建</el-button>
        <el-button type="success" @click="importFile">导入</el-button>
        <el-button type="warning" @click="showExportDialog">导出</el-button>
      </el-header>
      <el-main>
        <el-row>
          <el-col :span="12">
            <div class="editorArea" ref="editorArea" @mouseenter="currentScrollArea = 'editor'"></div>
          </el-col>
          <el-col :span="12">
            <div class="previewArea" ref="previewArea" v-html="htmlStr" @scroll="onPreviewScroll"
              @mouseenter="currentScrollArea = 'preview'"></div>
          </el-col>
        </el-row>
      </el-main>
    </el-container>
  </div>
</template>

<script setup>
import CodeMirror from "codemirror";
import "codemirror/mode/markdown/markdown.js";
import "codemirror/lib/codemirror.css";
import { ElHeader, ElContainer, ElMain, ElButton, ElMessageBox } from 'element-plus';
import { onMounted, ref } from "vue";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
// rehype-highlight原仓库的代码中执行toText函数时未传第二个参数，本地开发正常，打包后线上访问报错，所以复制了其代码进行了修改
// import rehypeHighlight from "rehype-highlight";
import rehypeHighlight from "./lib/rehype-highlight";
import remarkGfm from "remark-gfm";
import "highlight.js/styles/agate.css";
import "./themes/sspai.less";
import xml from "highlight.js/lib/languages/xml";
import bash from "highlight.js/lib/languages/bash";
import css from "highlight.js/lib/languages/css";
import markdown from "highlight.js/lib/languages/markdown";
import java from "highlight.js/lib/languages/java";
import javascript from "highlight.js/lib/languages/javascript";
import json from "highlight.js/lib/languages/json";
import less from "highlight.js/lib/languages/less";
import php from "highlight.js/lib/languages/php";
import plaintext from "highlight.js/lib/languages/plaintext";
import python from "highlight.js/lib/languages/python";
import scss from "highlight.js/lib/languages/scss";
import sql from "highlight.js/lib/languages/sql";
import typescript from "highlight.js/lib/languages/typescript";
import c from "highlight.js/lib/languages/c";
import diff from "highlight.js/lib/languages/diff";
import shell from "highlight.js/lib/languages/shell";
import rust from "highlight.js/lib/languages/rust";
import swift from "highlight.js/lib/languages/swift";
import article from './article.md?raw';

let editor = null;
const editorArea = ref(null);
const previewArea = ref(null);
const htmlStr = ref("");
let treeData = null;
let currentScrollArea = ref("");
const editorContentKey = 'editorContent'; // 用于localStorage的key

// 获取语法树
const customPlugin = () => (tree) => {
  treeData = tree;
};
// 计算位置信息
let editorElementList = [];
let previewElementList = [];

const saveToLocalStorage = (content) => {
  localStorage.setItem(editorContentKey, content);
};

const loadFromLocalStorage = () => {
  return localStorage.getItem(editorContentKey) || '';
};

const newFile = () => {
  ElMessageBox.confirm('新增文件会清空本地缓存数据，是否新增？', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // 清空缓存和编辑器内容
    localStorage.removeItem(editorContentKey);
    editor.setValue('');
  }).catch(() => {
    // 点击取消，不做任何操作
  });
};

const importFile = () => {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = '.md';
  input.onchange = (event) => {
    const file = event.target.files[0];
    const reader = new FileReader();
    reader.onload = (e) => {
      editor.setValue(e.target.result);
    };
    reader.readAsText(file);
  };
  input.click();
};

const showExportDialog = () => {
  ElMessageBox.prompt('请输入文件名称', '导出MarkDown文件', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    inputPattern: /^[^\s]+(\.md)?$/,
    inputErrorMessage: '文件名称不能包含空格，可选扩展名 .md'
  }).then(({ value }) => {
    const fileName = value.endsWith('.md') ? value : `${value}.md`;
    exportToFile(fileName);
  }).catch(() => {
    // 点击取消，不做任何操作
  });
};

const exportToFile = (fileName) => {
  const content = editor.getValue();
  const blob = new Blob([content], { type: 'text/markdown' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
};

const computedPosition = () => {
  let previewChildNodes = previewArea.value.childNodes;
  // 清空数组
  editorElementList = [];
  previewElementList = [];
  treeData.children.forEach((child, index) => {
    // 如果节点类型不为element则跳过
    if (child.type !== "element") {
      return;
    }
    let offsetTop = editor.heightAtLine(child.position.start.line - 1, "local"); // 设为local返回的坐标是相对于编辑器本身的，其他还有两个可选项：window、page
    // 保存元素的位置信息
    editorElementList.push(offsetTop);
    previewElementList.push(previewChildNodes[index].offsetTop); // 预览区域的容器元素previewArea需要设置定位
  });
};
// 编辑区域的滚动事件
const onEditorScroll = () => {
  if (currentScrollArea.value !== "editor") {
    return;
  }
  computedPosition();
  // 获取编辑器滚动信息
  let editorScrollInfo = editor.getScrollInfo();
  // 找出当前滚动到元素索引
  let scrollElementIndex = null;
  for (let i = 0; i < editorElementList.length; i++) {
    if (editorScrollInfo.top < editorElementList[i]) {
      // 当前元素的offsetTop大于滚动的距离，相当于当前滚动到了前一个元素上
      scrollElementIndex = i - 1;
      break;
    }
  }
  // 已经滚动到底部
  if (
    editorScrollInfo.top >=
    editorScrollInfo.height - editorScrollInfo.clientHeight
  ) {
    previewArea.value.scrollTop =
      previewArea.value.scrollHeight - previewArea.value.clientHeight;
    return;
  }
  if (scrollElementIndex >= 0) {
    // 设置预览区域的滚动距离为对应元素的offsetTop
    let ratio =
      (editorScrollInfo.top - editorElementList[scrollElementIndex]) /
      (editorElementList[scrollElementIndex + 1] -
        editorElementList[scrollElementIndex]);
    previewArea.value.scrollTop =
      ratio *
      (previewElementList[scrollElementIndex + 1] -
        previewElementList[scrollElementIndex]) +
      previewElementList[scrollElementIndex];
  }
};
// 预览区域的滚动事件
const onPreviewScroll = () => {
  if (currentScrollArea.value !== "preview") {
    return;
  }
  computedPosition();
  let previewScrollTop = previewArea.value.scrollTop;
  // 找出当前滚动到元素索引
  let scrollElementIndex = null;
  for (let i = 0; i < previewElementList.length; i++) {
    if (previewScrollTop < previewElementList[i]) {
      scrollElementIndex = i - 1;
      break;
    }
  }
  // 已经滚动到底部
  if (
    previewScrollTop >=
    previewArea.value.scrollHeight - previewArea.value.clientHeight
  ) {
    let editorScrollInfo = editor.getScrollInfo();
    editor.scrollTo(0, editorScrollInfo.height - editorScrollInfo.clientHeight);
    return;
  }
  if (scrollElementIndex >= 0) {
    // 设置预览区域的滚动距离为对应元素的offsetTop
    let ratio =
      (previewScrollTop - previewElementList[scrollElementIndex]) /
      (previewElementList[scrollElementIndex + 1] -
        previewElementList[scrollElementIndex]);
    let editorScrollTop =
      ratio *
      (editorElementList[scrollElementIndex + 1] -
        editorElementList[scrollElementIndex]) +
      editorElementList[scrollElementIndex];
    editor.scrollTo(0, editorScrollTop);
  }
};
const onChange = () => {
  unified()
    .use(remarkParse) // 将markdown转换成语法树
    .use(remarkGfm) // 支持GFM (tables, autolinks, tasklists, strikethrough)
    .use(remarkRehype) // 将markdown语法树转换成html语法树，转换之后就可以使用rehype相关的插件
    .use(rehypeHighlight, {
      ignoreMissing: true,
      languages: {
        xml,
        bash,
        css,
        markdown,
        java,
        javascript,
        json,
        less,
        php,
        plaintext,
        python,
        scss,
        sql,
        typescript,
        c,
        diff,
        shell,
        rust,
        swift
      },
    }) // 代码块高亮
    .use(customPlugin)
    .use(rehypeStringify) // 将html语法树转换成html字符串
    .process(editor.getValue())
    .then(
      (file) => {
        htmlStr.value = String(file);
      },
      (error) => {
        throw error;
      }
    );
};
onMounted(() => {
  const initialContent = loadFromLocalStorage();
  editor = CodeMirror(editorArea.value, {
    value: initialContent,
    mode: "markdown",
    lineNumbers: true,
    lineWrapping: true,
  });
  editor.on("change", (cm, change) => {
    onChange();
    saveToLocalStorage(cm.getValue());
  });
  editor.on("scroll", onEditorScroll);
  onChange();
});
</script>

<style lang="less">
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
</style>
<style lang="less" scoped>
.container {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;

  .header-buttons {
    display: flex;
    align-items: center;
    /* 上下居中 */
    // justify-content: center;
    /* 水平居中 */
  }

  .editorArea {
    // width: 50%;
    height: 100%;
    border-right: 1px solid #e1e4e8;
    flex-shrink: 0;

    :deep(.CodeMirror) {
      height: 100%;
      font-size: 16px;
      font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
      box-sizing: border-box;
      background-color: #f8f9fa;
    }
  }

  .previewArea {
    position: relative;
    // width: 50%;
    height: 100%;
    padding: 20px;
    overflow: auto;
    flex-shrink: 0;
  }
}
</style>
